﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace FluentBinding.WinForms
{
    ///<summary>
    /// Represents component which function as proxy between data-bound control and entity    
    /// </summary>
    /// <typeparam name="E">Type of the root entity</typeparam>
    public class BindingProxy<E> : INotifyPropertyChanged
    {
        #region Fields

        public const string DataMemberName = "DataMember";
        private readonly List<string> _affectedEntities = new List<string>();

        private readonly Dictionary<INotifyPropertyChanged, PropertyNotifier> _notifiers =
            new Dictionary<INotifyPropertyChanged, PropertyNotifier>();

        private IDataMemberConvertor<E> _dataMemberConvertor;
        private string _entityPath;
        private bool _isUpdatingEntity;
        private PropertyChangedEventHandler _propertyChanged = delegate { };
        private SynchronizationContext _uiContext;
        private List<Binding> _bindings = new List<Binding>();

        #endregion

        #region Constructors

        public BindingProxy(E entity, Control[] controls)
        {            
            Entity = entity;
            Controls = controls;

            _uiContext = SynchronizationContext.Current;

            if(_uiContext == null)
                throw new InvalidOperationException("SynchronizationContext is not available. Binding has to be initialized in UI thread");
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged
        {
            add
            {
                // empty delegate
                if (_propertyChanged.GetInvocationList().Length == 1)
                    _notifiers.Keys.ForEach(x => x.PropertyChanged += PublisherPropertyChanged);

                _propertyChanged += value;
            }
            remove
            {
                _propertyChanged -= value;

                // empty delegate
                if (_propertyChanged.GetInvocationList().Length == 1)
                    _notifiers.Keys.ForEach(x => x.PropertyChanged -= PublisherPropertyChanged);
            }
        }

        #endregion

        #region Public properties

        public E Entity { get; private set; }

        public Control[] Controls { get; private set; }

        public object DataMember
        {
            get { return _dataMemberConvertor.Select(Entity); }
            set
            {
                try
                {
                    _isUpdatingEntity = true;
                    _dataMemberConvertor.Initialize(Entity, value);
                    DataMemberUpdated(this, EventArgs.Empty);
                }
                finally
                {
                    _isUpdatingEntity = false;
                }
            }
        }

        public DataSourceUpdateMode DataSourceUpdateMode
        {
            get
            {
                if (_bindings.Count == 0)
                    throw new InvalidOperationException(
                        string.Format("{0} is not initialized", GetType().Name));

                return _bindings[0].DataSourceUpdateMode;
            }
            set
            {
                _bindings.ForEach(x => x.DataSourceUpdateMode = value);
            }
        }

        public IEnumerable<Binding> Bindings
        {
            get { return _bindings; }
        }

        #endregion

        #region Private or internal properties

        internal string EntityPath
        {
            get { return _entityPath; }
            set
            {
                _entityPath = value;
                EntityPathUpdated(this, EventArgs.Empty);
            }
        }

        internal IEnumerable<string> AffectedEntities
        {
            get { return _affectedEntities; }
        }

        #endregion

        #region Public methods

        public void BindTo(string controlPropertyName)
        {
            _bindings.AddRange(Controls.Select(x => x.DataBindings.Add(controlPropertyName, this, DataMemberName)));
        }

        public void AddAffectedEntity(string entityPath)
        {
            _affectedEntities.Add(entityPath);
        }

        /// <summary>
        /// Add notifier with all property names        
        /// </summary>
        public void AddNotifier(INotifyPropertyChanged notifierEntity)
        {
            PropertyNotifier notifier = GetOrCreatePropertyNotifier(notifierEntity);

            notifier.RegisterAllProperties();
        }

        /// <summary>
        /// Add notifier with specified property names        
        /// </summary>
        public void AddNotifier(INotifyPropertyChanged notifierEntity, string propertyName)
        {
            PropertyNotifier notifier = GetOrCreatePropertyNotifier(notifierEntity);

            notifier.RegisterPropertyName(propertyName);
        }

        public void AssignDataMemberConvertor(IDataMemberConvertor<E> dataMemberConvertor)
        {
            _dataMemberConvertor = dataMemberConvertor;
        }

        public void NotifyEntityIsUpdated()
        {
            RaisePropertyChangedInUIThread();            
        }

        #endregion

        #region Private or internal methods

        private PropertyNotifier GetOrCreatePropertyNotifier(INotifyPropertyChanged notifierEntity)
        {
            PropertyNotifier notifier = _notifiers.TryGetValue(notifierEntity);

            if (notifier == null)
            {
                notifier = new PropertyNotifier(notifierEntity);
                _notifiers.Add(notifierEntity, notifier);
            }

            return notifier;
        }

        #endregion

        public event EventHandler EntityPathUpdated = delegate { };
        public event EventHandler DataMemberUpdated = delegate { };

        #region Event handlers

        private void PublisherPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            var notifierEntity = sender as INotifyPropertyChanged;

            if (notifierEntity != null)
            {
                if (GetOrCreatePropertyNotifier(notifierEntity).IsRegistered(args.PropertyName))
                    RaisePropertyChangedInUIThread();

                if (!_isUpdatingEntity)
                    DataMemberUpdated(this, EventArgs.Empty);
            }
        }

        private void RaisePropertyChangedInUIThread()
        {
            //_propertyChanged(this, new PropertyChangedEventArgs(DataMemberName));

            _uiContext.Send(state =>
                _propertyChanged(this, new PropertyChangedEventArgs(DataMemberName)), null);
        }

        #endregion
    }
}